﻿using System;
using System.ComponentModel;
using System.Linq;
using System.Xml;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;
using Notoric.Model.Validation;

namespace Notoric.Model.Elements.CWNotation
{
    /// <summary>
    /// An abstract base class for symbols that have a duration (notes and rests).
    /// </summary>
    /// <remarks>
    /// The <see cref="DurationalSymbol"/> class is responsible for controlling the symbol duration
    /// including the management of augmentation dots.
    /// </remarks>
    public abstract class DurationalSymbol : CoreSymbol
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="DurationalSymbol"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="Staff"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="presentedElement">
        /// <para>
        /// Type: <see cref="DurationalSymbolElement"/>
        /// </para>
        /// <para>
        /// The durational symbol presented by this instance.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        protected DurationalSymbol(XmlPackagePart packagePart, Staff owner, DurationalSymbolElement presentedElement)
            : base(packagePart, owner)
        {
            if (presentedElement == null)
                throw new ArgumentNullException("presentedElement");

            this.presentedElement = presentedElement;
            presentedElementIDAttribute.Value = presentedElement.ID.ToString();
            presentedElement.PropertyChanged += new PropertyChangedEventHandler(presentedElement_PropertyChanged);

            ushort basicDuration;
            byte dots;
            DecomposeDuration(presentedElement.Duration, out basicDuration, out dots);
            dotsAttribute.Value = dots.ToString(PackageVocabulary.PackageCulture.NumberFormat);
            durationAttribute.Value = basicDuration.ToString(PackageVocabulary.PackageCulture.NumberFormat);
            beam = null;
            beamIdAttribute.Value = "0";
        }

        /// <summary>
        /// Creates a new <see cref="DurationalSymbol"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="Staff"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        protected DurationalSymbol(XmlPackagePart packagePart, Staff owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            beamIdAttribute = element.Attributes[PackageVocabulary.BeamAttribute];
            dotsAttribute = element.Attributes[PackageVocabulary.DotsAttribute];
            durationAttribute = element.Attributes[PackageVocabulary.DurationAttribute];
            presentedElementIDAttribute = element.Attributes[PackageVocabulary.IDRefAttribute];

            var peId = new Guid(presentedElementIDAttribute.Value);
            presentedElement = packagePart.Package.IDMap.FindObject<DurationalSymbolElement>(peId);

            if (presentedElement == null)
                throw new SerializationException("The presented durational symbol not found!", element.OuterXml);
        }

        #endregion

        #region CoreSymbol Members

        /// <inheritdoc/>
        protected override XmlElement CreateSymbolElement(XmlDocument document)
        {
            var element = CreateSymbolElementCore(document);

            beamIdAttribute = document.CreateAttribute(PackageVocabulary.BeamAttribute);
            dotsAttribute = document.CreateAttribute(PackageVocabulary.DotsAttribute);
            durationAttribute = document.CreateAttribute(PackageVocabulary.DurationAttribute);
            presentedElementIDAttribute = document.CreateAttribute(PackageVocabulary.IDRefAttribute);

            element.Attributes.Append(beamIdAttribute);
            element.Attributes.Append(dotsAttribute);
            element.Attributes.Append(durationAttribute);
            element.Attributes.Append(presentedElementIDAttribute);

            return element;
        }

        /// <inheritdoc/>
        public override BeatElement Beat
        {
            get { return presentedElement.FindOwner<BeatElement>(); }
            set
            {
                ValidateAndThrow("Beat", value);

                var beat = presentedElement.FindOwner<BeatElement>();
                
                beat.RemoveSymbol(presentedElement);
                presentedElement.MigrateTo(value);
                value.AddExistingSymbol(presentedElement);

                RaisePropertyChanged("Beat");
            }
        }

        /// <inheritdoc/>
        public override MeasureElement Measure
        {
            get { return presentedElement.FindOwner<MeasureElement>(); }
            set
            {
                throw new NotImplementedException();
            }
        }

        /// <inheritdoc/>
        public override double Width
        {
            get 
            {
                if (AugmentationDots == 0)
                    return GetCoreWidth();
                else
                {
                    var staff = FindOwner<Staff>();

                    var width = GetCoreWidth();
                    width += staff.SpaceSize / 2 + ((staff.SpaceSize * 0.58) * (AugmentationDots - 1));

                    return width;
                }
            }
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the value of some property of the <see cref="presentedElement"/> has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// If the property that has changed is "Duration" the handler recomputes the <see cref="BasicDuration"/> and <see cref="AugmentationDots"/>.<br/>
        /// </remarks>
        /// <param name="sender">Object that has raised the event</param>
        /// <param name="e">Event data</param>
        private void presentedElement_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "Duration")
            {
                ushort basicDuration;
                byte dots;

                DecomposeDuration(presentedElement.Duration, out basicDuration, out dots);

                if (basicDuration != BasicDuration)
                {
                    durationAttribute.Value = basicDuration.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                    RaisePropertyChanged("BasicDuration");
                }
                if (dots != AugmentationDots)
                {
                    dotsAttribute.Value = basicDuration.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                    RaisePropertyChanged("AugmentationDots");
                }
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Computes the total duration out of the basic duration and the number of augmentation dots.
        /// </summary>
        /// <param name="basicDuration">Basic duration (without augmentation dots)</param>
        /// <param name="dots">The number of augmentation dots</param>
        /// <returns>The calculated total duration</returns>
        private ushort CalculateDuration(ushort basicDuration, byte dots)
        {
            ushort increment = (ushort)(basicDuration / 2);
            ushort duration = basicDuration;

            for (int i = 0; i < dots; ++i)
            {
                duration += increment;
                increment /= 2;
            }

            return duration;
        }

        /// <summary>
        /// Decomposes the given duration value to the basic duration and number of augmentation dots.
        /// </summary>
        /// <param name="duration">Total duration</param>
        /// <param name="basicDuration">Calculated basic duration (without augmentation dots)</param>
        /// <param name="dots">Calculated number of the augmentation dots</param>
        private void DecomposeDuration(ushort duration, out ushort basicDuration, out byte dots)
        {
            if (duration <= DurationalSymbolElement.DoubleWhole)
            {
                basicDuration = Utils.Duration.Highest2Pow(duration);
                
                var bitMask = basicDuration >> 1;
                dots = 0;						
                while ((duration & bitMask) != 0)
                {
                    bitMask >>= 1;
                    ++dots;
                }
            }
            else
            {
                basicDuration = DurationalSymbolElement.DoubleWhole;
                dots = 0;
            }
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// When overriden in a derived class, this method creates the XML element that holds
        /// the data of this object.
        /// </summary>
        /// <remarks>
        /// The attributes for the inherited properties and the type of the class
        /// are added by the base classes.
        /// </remarks>
        /// <param name="document"></param>
        /// <returns></returns>
        protected abstract XmlElement CreateSymbolElementCore(XmlDocument document);
        
        #endregion

        #region Public Methods

        /// <summary>
        /// When overriden in a derived class, this method returns the width of the symbol
        /// without the augmentation dots (they are added by <see cref="DurationalSymbol"/> class).
        /// </summary>
        /// <returns>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The width (in centimeters) of the symbol (without the augmentation dots).
        /// </para>
        /// </returns>
        public abstract double GetCoreWidth();

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the number of augmentation dots belonging to this symbol.
        /// </summary>
        /// <remarks>
        /// The setter of this property just recomputes the total duration of the presented
        /// symbol and sets it. The values of the <see cref="BasicDuration"/> and <see cref="AugmentationDots"/> properties
        /// is modified afterwards in response to the <see cref="ModelElement.PropertyChanged"/> event from the presented symbol.
        /// </remarks>
        /// <value>
        /// <para>
        /// Type: <see cref="Byte"/>
        /// </para>
        /// <para>
        /// The number of augmentation dots belonging to this symbol.
        /// Each augmentation dot prolongates the duration
        /// of the symbol by one half.
        /// </para>
        /// </value>
        /// <example>
        /// If the <see cref="BasicDuration"/> equals to <see cref="DurationalSymbolElement.Quarter"/> then
        /// <list type="bullet">
        /// <item>
        /// With one augmentation dot the actual duration will be <see cref="DurationalSymbolElement.Quarter"/> + <see cref="DurationalSymbolElement.Eighth"/>
        /// </item>
        /// <item>
        /// With two augmentation dots the actual duration will be <see cref="DurationalSymbolElement.Quarter"/> + <see cref="DurationalSymbolElement.Eighth"/>
        /// + <see cref="DurationalSymbolElement.Sixteenth"/>
        /// </item>
        /// <item>
        /// etc...
        /// </item>
        /// </list>
        /// </example>
        public byte AugmentationDots
        {
            get { return byte.Parse(dotsAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                presentedElement.Duration = CalculateDuration(BasicDuration, value);
            }
        }

        /// <summary>
        /// Gets or sets the basic duration of the symbol.
        /// </summary>
        /// <remarks>
        /// The setter of this property just recomputes the total duration of the presented
        /// symbol and sets it. The values of the <see cref="BasicDuration"/> and <see cref="AugmentationDots"/> properties
        /// is modified afterwards in response to the <see cref="ModelElement.PropertyChanged"/> event from the presented symbol.
        /// </remarks>
        /// <value>
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The duration of the symbol without the augmentation dots.<br/>
        /// This value must not be 0.
        /// </para>
        /// </value>
        /// <exception cref="ValidationException">
        /// The <paramref name="value"/> is 0.
        /// </exception>
        /// TODO: Write the validation rules.
        public ushort BasicDuration
        {
            get { return ushort.Parse(durationAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                ValidateAndThrow("BasicDuration", value);

                presentedElement.Duration = CalculateDuration(value, AugmentationDots);
            }
        }

        /// <summary>
        /// Gets or sets the beam that joins the group that this symbol is a part of.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Beam"/>
        /// </para>
        /// <para>
        /// The beam that joins the group that this symbol is a part of.
        /// <see langword="null"/> means that the symbol is not a part of any group.
        /// </para>
        /// </value>
        public Beam GroupBeam
        {
            get 
            {
                if (beam == null && beamIdAttribute.Value != "0")
                {
                    var beamId = new Guid(beamIdAttribute.Value);
                    beam = PackagePart.Package.IDMap.FindObject<Beam>(beamId);

                    if (beam == null)
                        throw new SerializationException("The beam id " + beamId + " could not be deserialized!", Element.OuterXml);
                }
                return beam; 
            }
            set
            {
                beam = value;
                beamIdAttribute.Value = (beam != null ? beam.ID.ToString() : "0");

                RaisePropertyChanged("GroupBeam");
            }
        }

        /// <summary>
        /// Gets a reference to the <see cref="DurationalSymbolElement"/> that is presented by this <see cref="DurationalSymbol"/>.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="DurationalSymbolElement"/>
        /// </para>
        /// <para>
        /// The duration symbol from the presented composition that is being presented by this symbol.
        /// </para>
        /// </value>
        public DurationalSymbolElement PresentedElement
        {
            get { return presentedElement; }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="GroupBeam"/> property.
        /// </summary>
        private Beam beam;
        /// <summary>
        /// Holds the ID of the <see cref="GroupBeam"/>.
        /// </summary>
        private XmlAttribute beamIdAttribute;
        /// <summary>
        /// Holds the value of the <see cref="AugmentationDots"/> property.
        /// </summary>
        private XmlAttribute dotsAttribute;
        /// <summary>
        /// Holds the value of the <see cref="BasicDuration"/> property.
        /// </summary>
        private XmlAttribute durationAttribute;
        /// <summary>
        /// Holds the value of the <see cref="PresentedElement"/> property.
        /// </summary>
        private DurationalSymbolElement presentedElement;
        /// <summary>
        /// Holds the ID of the <see cref="PresentedElement"/>.
        /// </summary>
        private XmlAttribute presentedElementIDAttribute;

        #endregion
    }
}
